/**
 * 这里主要封装了RTC多对多的媒体协商过程，一些参数和格式需要前后端保持一致，如果没有特殊要求的话，开箱即用
 * author czl
 * date 2024/04/19
 */

/**
 * 扩展WebSocket的send方法，定义了三个参数，分别对应事件类型、数据、指定用户，最终以JSON数组的格式发送到后端
 * @param type
 * @param data
 * @param sendToUser 非必填，不指定的话就是广播给除自己外的其它用户
 */
WebSocket.prototype.convertAndSend = function (type, data, sendToUser) {
  this.send(JSON.stringify([type, data, sendToUser]))
}

/**
 * 扩展一个实现多对多的RTC class, new一个实例的时候需要传入下面这些参数
 * @param ops {
 *  wsUrl: String, //websocket连接地址
 *  rtcConfig: Object, //原生的RTC服务器配置
 *  localStream: MediaStream, //本地媒体流（音频、视频、音视频均可）
 *  remoteStreams: Array<MediaStream> //远端的媒体流数组，后续媒体协商完成后会把流添加到该数组中，需要自己监听这个数组的变化来动态处理这些流
 * }
 * @constructor
 */
function SimpleWebRTC(ops) {
  const {
    wsUrl,
    rtcConfig,
    localStream,
    remoteStreams = []
  } = ops
  this.pcMap = new Map()
  this.rtcConfig = rtcConfig
  this.localStream = localStream
  this.remoteStreams = remoteStreams
  this.socket = this.initSocket(wsUrl)
}

/**
 * 初始化websocket，进行信令通信
 * 加入房间（joined）、离开房间（leaved）、媒体协商（invite、offer、answer、candidate）这些事件都是通用逻辑，没有特殊要求不用去改
 * @param wsUrl
 * @returns {WebSocket}
 */
SimpleWebRTC.prototype.initSocket = function (wsUrl) {
  const socket = new WebSocket(wsUrl)
  const rtc = this

  socket.onopen = function (e) {
    console.log('onopen', e)
    rtc.onopen?.bind(rtc)(e) //如果有自定义onopen回调的话
  }

  socket.onclose = function (e) {
    console.log('onclose', e)
    rtc.onclose?.bind(rtc)(e) //如果有自定义onclose回调的话
  }

  socket.onmessage = function (e) {
    let payload = JSON.parse(e.data)
    let type = payload[0]
    let data = payload[1]
    let remoteUser = payload[2] //发送消息时指定了sendToUser才会有这个值，表示由谁发送。比如A发送给B，B收到的消息这里的值为A

    switch (type) {
      case 'joined': //所有加入房间的用户都会收到这个消息
        rtc.users = data
        rtc.onjoined?.bind(rtc)(remoteUser)
        break
      case 'invite': //除了当前新加入房间的用户，其他用户都会收到这个消息
        rtc.sendOfferToUser(data)
        break
      case 'leaved': //除了当前离开房间的用户，其他用户都会收到这个消息
        rtc.users = data
        let index = rtc.remoteStreams.map(stream => stream.user).indexOf(remoteUser)
        if (index !== -1) {
          rtc.remoteStreams.splice(index, 1)
        }

        let pc = rtc.pcMap.get(remoteUser)
        if (pc) {
          pc.close()
          rtc.pcMap.delete(remoteUser)
        }

        rtc.onleaved?.bind(rtc)(remoteUser)
        break
      case 'offer':
        rtc.receiveOfferAndSendAnswer(data, remoteUser)
        break
      case 'answer':
        rtc.receiveAnswer(data, remoteUser)
        break
      case 'candidate':
        rtc.handleCandidate(data, remoteUser)
        break
      default:
        rtc.onmessage?.bind(rtc)(type, data, remoteUser) //处理其它消息的回调，如果有定义的话
    }
  }

  return socket
}

SimpleWebRTC.prototype.sendOfferToUser = async function (sendToUser) {
  console.log(`向${sendToUser}发出offer`)
  let pc = this.pcMap.get(sendToUser)
  if (!pc) {
    pc = new RTCOnSocketPeerConnection({
      remoteUser: sendToUser,
      remoteStreams: this.remoteStreams,
      localStream: this.localStream,
      socket: this.socket,
      ...this.rtcConfig
    })
    this.pcMap.set(sendToUser, pc)
  }

  await pc.createOffer()
}

SimpleWebRTC.prototype.receiveOfferAndSendAnswer = async function (offer, remoteUser) {
  console.log(`收到${remoteUser}的offer`)
  const pc = new RTCOnSocketPeerConnection({
    remoteUser,
    remoteStreams: this.remoteStreams,
    localStream: this.localStream,
    socket: this.socket,
    ...this.rtcConfig
  })
  this.pcMap.set(remoteUser, pc)

  await pc.setRemoteDescription(offer)
  await pc.createAnswer()
}

SimpleWebRTC.prototype.receiveAnswer = async function (answer, remoteUser) {
  let pc = this.pcMap.get(remoteUser)
  if (!pc) {
    console.error('')
    return
  }

  await pc.setRemoteDescription(answer)
}

SimpleWebRTC.prototype.handleCandidate = async function (candidate, remoteUser) {
  let pc = this.pcMap.get(remoteUser)
  if (!pc) {
    console.error('')
    return
  }

  // 添加ice
  await pc.addIceCandidate(candidate)
}

/**
 * 开放一个leave函数，可以在一些比如手动退出房间等场景下调用
 */
SimpleWebRTC.prototype.leave = function () {
  this.socket.close()
}

/**
 * 扩展一个RTCPeerConnection的封装，主要目的是为了方便信令通信以及后续的媒体协商
 * @param configuration
 * @constructor
 */
function RTCOnSocketPeerConnection(configuration) {
  this.remoteUser = configuration.remoteUser
  this.remoteStreams = configuration.remoteStreams
  this.localStream = configuration.localStream
  this.socket = configuration.socket
  this.pc = this.newPeerConnection(configuration)
}

RTCOnSocketPeerConnection.prototype.newPeerConnection = function (configuration) {
  let pc = new RTCPeerConnection(configuration)
  pc.onicecandidate = this.onicecandidate.bind(this)
  pc.ontrack = this.ontrack.bind(this)
  //将本地流添加到连接中，此时彼此都还收不到对方的流，要完成媒体协商（发送offer、响应answer、添加icecandidate）之后才会触发ontrack
  this.localStream.getTracks().forEach(track => pc.addTrack(track, this.localStream))
  return pc
}

RTCOnSocketPeerConnection.prototype.onicecandidate = function (event) {
  if (event.candidate) {
    this.socket.convertAndSend(
      'candidate',
      event.candidate,
      this.remoteUser
    )
  }
}

RTCOnSocketPeerConnection.prototype.ontrack = function (event) {
  let stream = event.streams[0]
  let find = this.remoteStreams.find(e => e.id === stream.id)
  if (!find) {
    stream.user = this.remoteUser
    this.remoteStreams.push(stream)
  }
}

RTCOnSocketPeerConnection.prototype.createOffer = async function () {
  let offer = await this.pc.createOffer()
  await this.pc.setLocalDescription(offer)
  this.socket.convertAndSend(
    'offer',
    offer,
    this.remoteUser
  )
}

RTCOnSocketPeerConnection.prototype.createAnswer = async function () {
  const answer = await this.pc.createAnswer()
  await this.pc.setLocalDescription(answer)
  this.socket.convertAndSend(
    'answer',
    answer,
    this.remoteUser
  )
}

RTCOnSocketPeerConnection.prototype.setRemoteDescription = function (sessionDescription) {
  this.pc.setRemoteDescription(sessionDescription)
}

RTCOnSocketPeerConnection.prototype.addIceCandidate = function (candidate) {
  this.pc.addIceCandidate(candidate)
}

RTCOnSocketPeerConnection.prototype.close = function () {
  this.pc.close()
}

// export default SimpleWebRTC